/*           Copyright(c) 2005-2008 Dave Sexton            *
 *                   HtmlEditor Control                    *
 * http://www.codeplex.com/DocProject/Project/License.aspx *
 ***********************************************************/
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml;
using DaveSexton.Controls.Resources;
using System.Collections.ObjectModel;

namespace DaveSexton.Controls
{
	public abstract class HtmlParserBase
	{
		#region Public Properties
		/// <summary>
		/// Specifies a list of tags that can be closed inside of a parent's sibling tag further in the document heirarchy instead of 
		/// inside the parent itself.
		/// </summary>
		/// <remarks>
		/// Add tags to this collection that may exist, offset from the valid flow of the document.  The default values are: 
		/// <c>font</c>, <c>strong</c>, <c>em</c> and <c>u</c>.
		/// <para>
		/// The <see cref="HtmlEditor"/> control may return HTML that uses the <c>font</c> tag, for example, in a manner that
		/// violates the xml document type.  By adding the <c>font</c> tag to this collection, <see cref="HtmlEditor"/> documents 
		/// that use them in this invalid manner can be processed by the <see cref="HtmlToXmlConverter"/> class and converted 
		/// into valid xml documents.
		/// </para>
		/// </remarks>
		/// <example>
		/// For example, if the <c>font</c> tag is added to this collection, then the following HTML
		/// <code>
		/// &lt;p&gt;
		///		&lt;font color=red&gt;
		///		red text
		/// &lt;/p&gt;
		/// &lt;p&gt;
		///		red text
		///		&lt;/font&gt;
		/// &lt;/p&gt;
		/// </code>
		/// will be parsed into 
		/// <code>
		/// &lt;p&gt;
		///		&lt;font color=&quot;red&quot;&gt;
		///		red text
		/// &lt;/font&gt;&lt;/p&gt;
		/// &lt;p&gt;
		///		&lt;font color=&quot;red&quot;&gt;
		///		red text
		///		&lt;/font&gt;
		/// &lt;/p&gt;
		/// </code>
		/// Otherwise, if <c>font</c> was not specified in this collection, then an exception would be thrown when the parser 
		/// reached the closing <c>font</c> tag since it doesn't have a corresponding start tag. 
		/// </example>
		public ICollection<string> AllowableOffsetTagNames
		{
			get
			{
				return new System.Collections.ObjectModel.ReadOnlyCollection<string>(new List<string>(offsetTags.Keys));
			}
		}

		public ICollection<string> EmptyTagNames
		{
			get
			{
				return new System.Collections.ObjectModel.ReadOnlyCollection<string>(singleTags);
			}
		}

		public ICollection<string> BooleanAttributeNames
		{
			get
			{
				return new System.Collections.ObjectModel.ReadOnlyCollection<string>(booleanAttributes);
			}
		}
		#endregion

		#region Private / Protected
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes")]
		public static readonly ICollection<string> DefaultAllowableOffsetTagNames = new ReadOnlyCollection<string>(new List<string>(new string[] { 
			"font", "strong", "em", "u" }));

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes")]
		public static readonly ICollection<string> DefaultEmptyTagNames = new ReadOnlyCollection<string>(new List<string>(new string[] { 
			"br", "hr", "img", "input", "link", "base", "meta", "area", "param", "col" }));

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes")]
		public static readonly ICollection<string> DefaultBooleanAttributeNames = new ReadOnlyCollection<string>(new List<string>(new string[] { 
			"nohref", "ismap", "declare", "checked", "disabled", "readonly", "multiple", "selected", "defer" }));

		protected static readonly char NewLineStart = Environment.NewLine[0];
		protected const char TagStart = '<';
		protected const char TagEnd = '>';
		protected const char ElementEnd = '/';
		protected const char AttributeQuote = '"';
		protected const char AttributeQuoteAlternate = '\'';
		protected const char CommentStart = '!';

		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")]
		protected TextReader reader;
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1051:DoNotDeclareVisibleInstanceFields")]
		[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Security", "CA2104:DoNotDeclareReadOnlyMutableReferenceTypes")]
		protected readonly XmlWriter writer;

		/// <summary>
		/// Dictionary where each key represents a unique tag name that can appear in the document with its start and end elements 
		/// offset by other elements.  The keys are used to find and fix this condition when html is transformed into valid xml.
		/// </summary>
		/// <remarks>
		/// For example, the following HTML fragment is valid: 
		/// <code>
		/// &lt;p&gt;&lt;font color=red&gt;&lt;/p&gt;&lt;p&gt;red text&lt;/font&gt;&lt;/p&gt;
		/// </code>
		/// Because <c>font</c> is added to this dictionary by default, the document above would be transformed into proper xml: 
		/// <code>
		/// &lt;p&gt;&lt;font color="red"&gt;&lt;/font&gt;&lt;/p&gt;&lt;p&gt;&lt;font color="red"&gt;red text&lt;/font&gt;&lt;/p&gt;
		/// </code>
		/// If <c>font</c> wasn't added to this dictionary then an exception would be thrown when the parser reached the closing
		/// <c>font</c> tag, stating that no start element was defined.
		/// <para>
		/// The key of this dictionary is a stack of attributes (name/value pairs).  The stack is filled with a tag's attributes as each 
		/// tag is located while parsing.  An element is popped from the stack when a closing tag is found with no start element, at which
		/// point subsequently parsed elements will no longer be wrapped with the tag's attributes since it has been closed at the current 
		/// parse point in the document.
		/// </para>
		/// <para>
		/// For example, in the second code block above, the <c>font</c> tag that appears in the last <c>p</c>
		/// tag was added because the stack that corresponds to the <c>font</c> key in this dictionary had one element that contained the
		/// attribute <c>color="red"</c>.  When the closing <c>font</c> tag is parsed, the attribute is popped, which means that subsequent
		/// paragraphs will not be wrapped with a new <c>font</c> tag.
		/// </para>
		/// </remarks>
		internal Dictionary<string, Stack<HtmlToXmlConverter.TagParser>> offsetTags;
		internal List<string> singleTags, booleanAttributes;
		#endregion

		#region Constructors
		/// <summary>
		/// Constructs a new instance of the <see cref="HtmlParserBase" /> class.
		/// </summary>
		protected HtmlParserBase(ICollection<string> allowableOffsetTagNames, ICollection<string> startElementOnlyTagNames, ICollection<string> booleanAttributeNames)
		{
			offsetTags = new Dictionary<string, Stack<HtmlToXmlConverter.TagParser>>(allowableOffsetTagNames.Count, StringComparer.OrdinalIgnoreCase);
			singleTags = new List<string>(startElementOnlyTagNames.Count);
			booleanAttributes = new List<string>(booleanAttributeNames.Count);

      AddAllowableOffsetTagNames(allowableOffsetTagNames ?? DefaultAllowableOffsetTagNames);
      AddEmptyTagNames(startElementOnlyTagNames ?? DefaultEmptyTagNames);

      if (booleanAttributeNames != null)
        AddBooleanAttributeNames(booleanAttributeNames);
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="HtmlParserBase" /> class for derived classes.
		/// </summary>
		protected HtmlParserBase(XmlWriter output)
			: this(DefaultAllowableOffsetTagNames, DefaultEmptyTagNames, DefaultBooleanAttributeNames)
		{
			if (output == null)
				throw new ArgumentNullException("output");

			this.writer = output;
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="HtmlParserBase" /> class for derived classes.
		/// </summary>
		protected HtmlParserBase(XmlWriter output, string[] allowableOffsetTagNames, string[] startElementOnlyTagNames, string[] booleanAttributeNames)
			: this(allowableOffsetTagNames, startElementOnlyTagNames, booleanAttributeNames)
		{
			if (output == null)
				throw new ArgumentNullException("output");

			this.writer = output;
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="HtmlParserBase" /> class for derived classes.
		/// </summary>
		protected HtmlParserBase(TextReader reader, XmlWriter output)
			: this(DefaultAllowableOffsetTagNames, DefaultEmptyTagNames, DefaultBooleanAttributeNames)
		{
			if (reader == null)
				throw new ArgumentNullException("reader");

			if (output == null)
				throw new ArgumentNullException("output");

			this.reader = reader;
			this.writer = output;
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="HtmlParserBase" /> class for derived classes.
		/// </summary>
		protected HtmlParserBase(TextReader reader, XmlWriter output, string[] allowableOffsetTagNames, string[] startElementOnlyTagNames, string[] booleanAttributeNames)
			: this(allowableOffsetTagNames, startElementOnlyTagNames, booleanAttributeNames)
		{
			if (reader == null)
				throw new ArgumentNullException("reader");

			if (output == null)
				throw new ArgumentNullException("output");

			this.reader = reader;
			this.writer = output;
		}

		/// <summary>
		/// Constructs a new instance of the <see cref="HtmlParserBase" /> class for derived classes.
		/// </summary>
		internal HtmlParserBase(TextReader reader, XmlWriter output, Dictionary<string, Stack<HtmlToXmlConverter.TagParser>> allowableOffsetTagNames, List<string> startElementOnlyTagNames, List<string> booleanAttributeNames)
		{
			if (reader == null)
				throw new ArgumentNullException("reader");

			if (output == null)
				throw new ArgumentNullException("output");

			this.reader = reader;
			this.writer = output;

			singleTags = startElementOnlyTagNames;
			offsetTags = allowableOffsetTagNames;
			booleanAttributes = booleanAttributeNames;
		}
		#endregion

		#region Methods
    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly")]
		[System.Diagnostics.DebuggerHidden]
		internal static void ThrowEndOfStreamException()
		{
			throw new ArgumentException(Errors.HtmlToXmlConverterEndOfStream, "html");
		}

    public void ClearAllowableOffsetTagNames()
    {
      offsetTags.Clear();
    }

    public void AddAllowableOffsetTagNames(IEnumerable<string> collection)
    {
      if (collection == null)
        throw new ArgumentNullException("collection");

      foreach (string tag in collection)
        if (!offsetTags.ContainsKey(tag.Trim()))
          offsetTags.Add(tag, new Stack<HtmlToXmlConverter.TagParser>());
    }

    public void ClearEmptyTagNames()
    {
      singleTags.Clear();
    }

    public void AddEmptyTagNames(IEnumerable<string> collection)
    {
      if (collection == null)
        throw new ArgumentNullException("collection");

      singleTags.AddRange(collection);
    }

    public void ClearBooleanAttributeNames()
    {
      booleanAttributes.Clear();
    }

    public void AddBooleanAttributeNames(IEnumerable<string> collection)
    {
      if (collection == null)
        throw new ArgumentNullException("collection");

      booleanAttributes.AddRange(collection);
    }

		/// <summary>
		/// Peeks at the next character in the current <see cref="StringReader" /> starting at the last position read.
		/// The specified <paramref name="predicate" /> is responsible for reading the character, which will move the iterator
		/// along; otherwise, the same character will be peeked at upon each iteration, infinitely.
		/// </summary>
		protected void PeekNextCharacter(bool canReachEndOfDocument, Predicate<char> predicate)
		{
			if (predicate == null)
				throw new ArgumentNullException("predicate");

			int i;

			while ((i = reader.Peek()) != -1)
				if (!predicate((char) i))
					return;

			if (!canReachEndOfDocument && i == -1)
				ThrowEndOfStreamException();
		}

		protected char PeekNextCharacter()
		{
			int i = reader.Peek();

			if (i == -1)
				ThrowEndOfStreamException();

			return (char) i;
		}

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly")]
		protected string ResolveEntity()
		{
			string name = ReadEntity();

      if (!name.StartsWith("#", StringComparison.Ordinal))
			{
				if (!HtmlEntities.Exists(name))
					throw new ArgumentException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
						Errors.HtmlToXmlConverterUnknownEntity, name), "html");

				name = HtmlEntities.GetValue(name);
			}

			return name;
		}

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly")]
		protected string ReadEntity()
		{
			StringBuilder entity = new StringBuilder(8);

			ReadNext();		// read '&'

			int next = reader.Peek();

			if (next == -1 || char.IsWhiteSpace((char) next))
				// escape ampersand character
				return "amp";

			int i;

			while ((i = reader.Read()) != -1)
			{
				char c = (char) i;

				if (c == ';')
					break;

				entity.Append(c);
			}

			if (i == -1)
				ThrowEndOfStreamException();

			if (entity.Length == 0)
				throw new ArgumentException(Errors.HtmlToXmlConverterMissingName, "html");

			return entity.ToString();
		}

    [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage", "CA2208:InstantiateArgumentExceptionsCorrectly")]
    protected string ReadName(bool endAtWhiteSpace, params char[] end)
		{
			StringBuilder name = new StringBuilder(16);

			int i;

			while ((i = reader.Peek()) != -1)
			{
				char c = (char) i;

				if (char.IsWhiteSpace(c))
				{
					if (endAtWhiteSpace)
						break;
					else
						throw new ArgumentException(string.Format(System.Globalization.CultureInfo.CurrentCulture,
							Errors.HtmlToXmlConverterWhiteSpaceInName, name), "html");
				}
				else if (Array.IndexOf(end, c) > -1)
					break;

				name.Append(char.ToLowerInvariant((char) reader.Read()));
			}

			if (i == -1)
				ThrowEndOfStreamException();

			if (name.Length == 0)
				throw new ArgumentException(Errors.HtmlToXmlConverterMissingName, "html");

			return name.ToString();
		}

    protected string ReadTo(bool resolveEntities, bool skipWhiteSpace, bool canReachEndOfDocument, params char[] any)
		{
			if (any == null)
				throw new ArgumentNullException("any");

			int i;

			StringBuilder text = new StringBuilder();

			while ((i = reader.Peek()) != -1)
			{
				char c = (char) i;

				if (resolveEntities && c == '&')
					text.Append("&" + ResolveEntity() + ";");
				else
				{
					if (Array.IndexOf(any, c) > -1)
						break;

					if (!char.IsWhiteSpace(c) || !skipWhiteSpace)
						text.Append(c);

					reader.Read();		// move next
				}
			}

			if (!canReachEndOfDocument && i == -1)
				ThrowEndOfStreamException();

			return text.ToString();
		}

		protected string ReadToContent(bool resolveEntities, bool canReachEndOfDocument)
		{
			int i;

			StringBuilder text = new StringBuilder();

			while ((i = reader.Peek()) != -1)
			{
				char c = (char) i;

				if (resolveEntities && c == '&')
					text.Append("&" + ResolveEntity() + ";");
				else
				{
					if (!char.IsWhiteSpace(c))
						break;

					text.Append(c);
					reader.Read();		// move next
				}
			}

			if (!canReachEndOfDocument && i == -1)
				ThrowEndOfStreamException();

			return text.ToString();
		}

		protected char ReadNext()
		{
			int i = reader.Read();

			if (i == -1)
				ThrowEndOfStreamException();

			return (char) i;
		}

		protected void SkipTo(bool skipEntities, bool canReachEndOfDocument, params char[] any)
		{
			int i;

			while ((i = reader.Peek()) != -1)
			{
				char c = (char) i;

				if ((!skipEntities && c == '&') || Array.IndexOf(any, c) > -1)
					break;

				reader.Read();		// skip
			}

			if (!canReachEndOfDocument && i == -1)
				ThrowEndOfStreamException();
		}

		protected void SkipToContent(bool skipEntities, bool canReachEndOfDocument)
		{
			int i;

			while ((i = reader.Peek()) != -1)
			{
				char c = (char) i;

				if ((!skipEntities && c == '&') || !char.IsWhiteSpace(c))
					break;

				reader.Read();		// skip
			}

			if (!canReachEndOfDocument && i == -1)
				ThrowEndOfStreamException();
		}

		protected bool SkipNext()
		{
			return reader.Read() != -1;
		}

		private void WriteWhiteSpace(char previousChar, bool collapseNewLine)
		{
			if (previousChar == NewLineStart && collapseNewLine)
				CollapseNewLine();
			else
				writer.WriteWhitespace(previousChar.ToString());
		}

		private void CollapseNewLine()
		{
			StringBuilder whitespace = new StringBuilder(NewLineStart.ToString(), Environment.NewLine.Length);

			// (skip first character)
			for (int x = 1; x < Environment.NewLine.Length; x++)
			{
				int i = reader.Read();

				if (i == -1)
					goto WriteAll;

				char c = (char) i;

				whitespace.Append(c);

				if (c != Environment.NewLine[x])
					goto WriteAll;
			}

			writer.WriteWhitespace("\n");
			return;
		WriteAll:
			writer.WriteWhitespace(whitespace.ToString());
		}

    protected bool MoveTo(bool resolveEntities, bool trimLeadingWhiteSpace, params char[] any)
		{
			return MoveToAny(resolveEntities, trimLeadingWhiteSpace, any) != -1;
		}

		protected void MoveTo(bool resolveEntities, bool trimLeadingWhiteSpace, bool canReachEndOfDocument, params char[] any)
		{
			if (MoveToAny(resolveEntities, trimLeadingWhiteSpace, any) == -1 && !canReachEndOfDocument)
				ThrowEndOfStreamException();
		}

		private int MoveToAny(bool resolveEntities, bool trimLeadingWhiteSpace, params char[] any)
		{
			if (any == null)
				throw new ArgumentNullException("any");

			bool writeWhiteSpace = !trimLeadingWhiteSpace;
			int i;

			while ((i = reader.Peek()) != -1)
			{
				char c = (char) i;

				if (resolveEntities && c == '&')
				{
					string entity = ResolveEntity();

					if (entity[0] == '#')
						writer.WriteRaw("&" + entity + ";");
					else
						writer.WriteEntityRef(entity);
				}
				else
				{
					if (Array.IndexOf(any, c) > -1)
						break;

					reader.Read();		// move next (before WriteWhiteSpace is called)

					if (char.IsWhiteSpace(c))
					{
						if (writeWhiteSpace)
						{
							if (c == (char) 160)
								// Non-breaking space cannot be written to the underlying writer as white-space; an exception is thrown.
								writer.WriteRaw("&#160;");
							else
								WriteWhiteSpace(c, true);
						}
					}
					else if (c == (char) 65279)		// byte order mark in Unicode documents
					{
						// ignore
					}
					else
					{
						writeWhiteSpace = true;
						writer.WriteRaw(c.ToString());
					}
				}
			}

			return i;
		}

		protected void WriteNext(bool writeWhiteSpace, bool collapseNewLine)
		{
			int i = reader.Read();

			if (i == -1)
				ThrowEndOfStreamException();

			char c = (char) i;

			if (char.IsWhiteSpace(c) && writeWhiteSpace)
				WriteWhiteSpace(c, collapseNewLine);
			else
				writer.WriteRaw(c.ToString());
		}
		#endregion
	}
}
